S11-08 Vue-进阶
[TOC]
自定义指令
自定义指令
在Vue的模板语法中我们学习过各种各样的指令:v-show、v-for、v-model等等,除了使用这些指令之外,Vue也允许我们来自定义自己的指令。
注意:在Vue中,代码的复用和抽象主要还是通过组件;
通常在某些情况下,你需要对DOM元素进行底层操作,这个时候就会用到自定义指令;
自定义指令分为两种:
自定义局部指令:组件中通过 directives 选项,只能在当前组件中使用;
自定义全局指令:app的 directive 方法,可以在任意组件中被使用;
比如我们来做一个非常简单的案例:当某个元素挂载完成后可以自定获取焦点
实现方式一:如果我们使用默认的实现方式;
实现方式二:自定义一个 v-focus 的局部指令;
实现方式三:自定义一个 v-focus 的全局指令;
实现方式一:聚焦的默认实现
实现方式二:局部自定义指令
实现方式二:自定义一个 v-focus 的局部指令
这个自定义指令实现非常简单,我们只需要在组件选项中使用 directives 即可;
它是一个对象,在对象中编写我们自定义指令的名称(注意:这里不需要加v-);
自定义指令有一个生命周期,是在组件挂载后调用的 mounted,我们可以在其中完成操作;
方式三:自定义全局指令
自定义一个全局的v-focus指令可以让我们在任何地方直接使用
指令的生命周期
一个指令定义的对象,Vue提供了如下的几个钩子函数:
created:在绑定元素的 attribute 或事件监听器被应用之前调用;
beforeMount:当指令第一次绑定到元素并且在挂载父组件之前调用;
mounted:在绑定元素的父组件被挂载后调用;
beforeUpdate:在更新包含组件的 VNode 之前调用;
updated:在包含组件的 VNode 及其子组件的 VNode 更新后调用;
beforeUnmount:在卸载绑定元素的父组件之前调用;
unmounted:当指令与元素解除绑定且父组件已卸载时,只调用一次;
指令的参数和修饰符
如果我们指令需要接受一些参数或者修饰符应该如何操作呢?
info是参数的名称;
aaa-bbb是修饰符的名称;
后面是传入的具体的值;
在我们的生命周期中,我们可以通过 bindings 获取到对应的内容:
案例:时间格式化指令
自定义指令案例:时间戳的显示需求:
在开发中,大多数情况下从服务器获取到的都是时间戳;
我们需要将时间戳转换成具体格式化的时间来展示;
在Vue2中我们可以通过过滤器来完成;
在Vue3中我们可以通过 计算属性(computed) 或者 自定义一个方法(methods) 来完成;
其实我们还可以通过一个自定义的指令来完成;
我们来实现一个可以自动对时间格式化的指令v-format-time:
- 这里我封装了一个函数,在首页中我们只需要调用这个函数并且传入app即可;
代码:
高阶组件
<Teleport>
认识Teleport
在组件化开发中,我们封装一个组件A,在另外一个组件B中使用:
那么组件A中template的元素,会被挂载到组件B中template的某个位置;
最终我们的应用程序会形成一颗DOM树结构;
但是某些情况下,我们希望组件不是挂载在这个组件树上的,可能是移动到Vue app之外的其他位置:
比如移动到body元素上,或者我们有其他的div#app之外的元素上;
这个时候我们就可以通过teleport来完成;
Teleport是什么呢?
它是一个Vue提供的内置组件,类似于react的Portals;
teleport翻译过来是心灵传输、远距离运输的意思;
- 它有两个属性:
- to:指定将其中的内容移动到的目标元素,可以使用选择器;
- disabled:是否禁用 teleport 的功能;
- 它有两个属性:
代码的效果
结合组件使用
当然,teleport也可以和组件结合一起来使用:
- 我们可以在 teleport 中使用组件,并且也可以给他传入一些数据;
多个teleport
如果我们将多个teleport应用到同一个目标上(to的值相同),那么这些目标会进行合并:
实现效果如下:
<Suspense>
异步组件和Suspense
注意:目前(2022-08-01)Suspense显示的是一个实验性的特性,API随时可能会修改。
Suspense是一个内置的全局组件,该组件有两个插槽:
default:如果default可以显示,那么显示default的内容;
fallback:如果default无法显示,那么会显示fallback插槽的内容;
自定义插件
认识Vue插件
通常我们向Vue全局添加一些功能时,会采用插件的模式,它有两种编写方式:
对象类型:一个对象,但是必须包含一个 install 的函数,该函数会在安装插件时执行;
函数类型:一个function,这个函数会在安装插件时自动执行;
插件可以完成的功能没有限制,比如下面的几种都是可以的:
添加全局方法或者 property,通过把它们添加到 config.globalProperties 上实现;
添加全局资源:指令/过滤器/过渡等;
通过全局 mixin 来添加一些组件选项;
一个库,提供自己的 API,同时提供上面提到的一个或多个功能;
插件的编写方式
对象类型的写法
函数类型的写法
h函数
h函数
Vue推荐在绝大数情况下使用模板来创建你的HTML,然后一些特殊的场景,你真的需要JavaScript的完全编程的能力,这个时候你可以使用 渲染函数 ,它比模板更接近编译器;
前面我们讲解过VNode和VDOM的概念:
Vue在生成真实的DOM之前,会将我们的节点转换成VNode,而VNode组合在一起形成一颗树结构,就是虚拟DOM (VDOM);
事实上,我们之前编写的 template 中的HTML 最终也是使用渲染函数生成对应的VNode;
那么,如果你想充分的利用JavaScript的编程能力,我们可以自己来编写 createVNode 函数,生成对应的VNode;
那么我们应该怎么来做呢?使用 h()函数:
h() 函数是一个用于创建 vnode 的一个函数;
其实更准备的命名是 createVNode() 函数,但是为了简便在Vue将之简化为 h() 函数;
基本使用
h()函数 如何使用呢?它接受三个参数:
注意事项:
如果没有props,那么通常可以将children作为第二个参数传入;
如果会产生歧义,可以将null作为第二个参数传入,将children作为第三个参数传入;
h函数可以在两个地方使用:
render函数选项中;
setup函数选项中(setup本身需要是一个函数类型,函数再返回h函数创建的VNode);
案例:计数器
JSX
babel配置
如果我们希望在项目中使用jsx,那么我们需要添加对jsx的支持:
jsx我们通常会通过Babel来进行转换(React编写的jsx就是通过babel转换的);
对于Vue来说,我们只需要在Babel中配置对应的插件即可;
安装Babel支持Vue的jsx插件:npm install @vue/babel-plugin-jsx -D
在babel.config.js配置文件中配置插件:
如果是Vite环境,需要安装插件:npm install @vitejs/plugin-vue-jsx -D
案例:计数器
响应式原理
响应式
案例:普通值的响应式:我们先来看一下响应式意味着什么?我们来看一段代码:
num有一个初始化的值,有一段代码使用了这个值;
那么在num有一个新的值时,这段代码可以自动重新执行;
响应式:上面的这样一种可以自动响应数据变化的代码机制,我们就称之为是响应式的。
案例:对象的响应式:那么我们再来看一下对象的响应式。
响应式实现
实现-响应式函数
设计
响应式函数:执行的代码中可能不止一行代码,所以我们可以将这些代码放到一个函数中。问题就变成了当数据发生变化时,自动去执行某个函数。
问题:在开发中我们是有很多的函数的,我们如何区分一个函数是否需要响应式。
下面的函数中 foo 需要在obj的name发生变化时,重新执行,做出相应。
bar函数是一个完全独立于obj的函数,它不需要执行任何响应式的操作。
手动收集
思路:区分一个函数是否需要响应式:手动收集响应式函数。
- 封装一个新的函数watchFn;
- 凡是传入到watchFn的函数,就是需要响应式的;
- 其他默认定义的函数都是不需要响应式的;
代码实现:
1、封装专门手动收集响应式函数的函数watchFn(),收集后立即执行一次函数。
2、调用watchFn()函数,传入响应式的函数foo、bar。
3、当属性变化时,遍历执行收集的函数。
封装-Depend
需求:监听多对象的响应式
目前我们收集的依赖是放到一个数组中来保存的,但是这里会存在数据管理的问题:
- 我们在实际开发中需要监听很多对象的响应式;
- 这些对象需要监听的不只是一个属性,它们很多属性的变化,都会有对应的响应式函数;
- 我们不可能在全局维护一大堆的数组来保存这些响应函数;
所以我们要设计一个类,这个类用于管理某一个对象的某一个属性的所有响应式函数:
封装Depend类:替代原来简单的 reactiveFns 数组
1、将上述的逻辑封装到一个Depend类。
2、调用类中的addDepend()
方法收集依赖。
3、调用watchFn()函数,传入响应式的函数foo、bar。
4、数据变化后,手动调用notify()
方法重新运行所有收集到的函数。
实现-监听对象变化
需求:监听对象变化:替代之前使用dep.notify()
手动运行响应式函数。
- Vue2:通过
Object.defineProperty()
的方式监听 - Vue3:通过
new Proxy()
的方式监听
代码实现:
Vue2:通过Object.defineProperty()
的方式监听
Vue3:通过new Proxy()
的方式监听
实现-自动收集依赖
对象依赖管理
需求:管理多对象的多属性依赖
我们目前是创建了一个Depend对象,用来管理对于name变化需要监听的响应函数:
- 但是实际开发中我们会有不同的对象,另外会有不同的属性需要管理;
- 我们如何可以使用一种数据结构来管理不同对象的不同依赖关系呢?
思路图解:在前面我们刚刚学习过WeakMap,并且在学习WeakMap的时候我讲到了后面通过WeakMap如何管理这种响应式的数据依赖。
- 1、dep对象数据结构的管理
- 每一个对象的每一个属性都会对应一个dep对象
- 同一个对象的多个属性的dep对象存放在一个map对象中
- 多个对象的map对象,存放在一个objMap对象(弱引用)中
- 2、依赖收集:当执行get函数时,自动添加fn函数
对象依赖管理-实现
我们可以写一个getDepend()函数专门来管理这种依赖关系:
1、通过Object.defineProperty()
函数的setter方法监听对象的修改,触发时回调setter方法。在其中生成dep实例,并返回保存到对象Map.get(属性)
的Map中的dep实例,对象修改时通过dep.notify()
方法重新运行收集到的响应函数。
2、封装 getDepend() 函数,负责通过obj的key生成/获取对应的Depend实例。
3、当对象属性变化时,会自动运行相关的响应函数。
5、
对象的依赖收集
问题:我们之前收集依赖的地方是在 watchFn 中,但是这种收集依赖的方式我们根本不知道是哪一个key的哪一个depend需要收集依赖,只能针对一个单独的depend对象来添加你的依赖对象。
思路:应该在调用Proxy或Object.defineProperty的get捕获器中收集依赖函数。
代码实现:
1、在watchFn()
方法中将监听的函数保存到全局变量reactiveFn中。
2、在getter
中访问到相关对象的属性时,将其添加到其对应的dep实例中。
3、当执行watchFn()
函数时,会在它的回调中访问到对象的属性,我们需要将这些属性自动收集起来。
细节补充
问题一:如果函数中有用到两次key,比如name,那么这个函数会被收集两次。
思路:不使用数组,而是使用Set保存收集的响应函数。
代码实现:
问题二:我们并不希望将添加reactiveFn放到get中,以为它是属于Dep的行为。
思路:添加一个新的方法,用于收集依赖。
代码实现:
1、去除defineProperty()
中对外部变量的依赖。
2、将对外部变量的依赖放入Depend类的depend方法中。
多个对象响应式
问题:目前的响应式是针对于obj一个对象的,其他对象无法实现响应式。
思路:封装reactive()函数,针对所有的对象都可以变成响应式对象。
代码实现:
1、封装reactive()函数。
2、使用reactive()函数创建一个响应式对象。
3、通过watchFn()
函数,收集依赖。
4、当对象变化时重新执行依赖该对象的函数。
重构-Vue3 Proxy
前面所实现的响应式的代码是Vue2的响应式原理。Vue3主要通过 Proxy 来监听数据的变化以及收集相关的依赖。
代码实现:Proxy + Reflect